Repository navigation
[OMNIML-6060] Load an expert-parallel plan through load_pretrained - #2780
shengliangxu wants to merge 11 commits into
Conversation
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review: 3
Please check the settings in the CodeRabbit UI or the
|
| Layer / File(s) | Summary |
|---|---|
Validate MoE distribution plans modelopt/torch/utils/plugins/model_load_utils.py, tests/unit/torch/utils/test_resolve_distribution_plan.py |
Plan resolution raises when routed experts would remain unsharded. Tests cover non-divisible expert counts, EP degree 1, and FSDP2 sharding as an alternative. |
Dispatch expert-parallel loads modelopt/torch/utils/plugins/model_load_utils.py, tests/unit/torch/utils/test_load_pretrained_distribution.py, CHANGELOG.rst |
load_pretrained routes EP plans through load_and_prepare_ep and passes the distribution plan and supported options. Unit tests cover dispatch and refusal cases. The changelog describes EP sharding and plan-resolution settings. |
Verify distributed expert sharding tests/gpu/torch/utils/test_load_pretrained_distribution.py |
GPU tests check that EP loading assigns contiguous expert slices to ranks and produces the expected distribution plan. |
Priority:
Estimated code review effort: 3 (Moderate) | ~20 minutes
Change: Feature
Sequence Diagram(s)
sequenceDiagram
participant load_pretrained
participant _load_sharded
participant load_and_prepare_ep
load_pretrained->>_load_sharded: Pass checkpoint, distribution plan, and load options
_load_sharded->>load_and_prepare_ep: Load checkpoint using EP plan and supported options
Merge Risk: 
Minimal · previous review dde99
-
Expert-parallel plans are routed to the EP loader with their options, and no current behavior defect warrants blocking merge. Add focused coverage for loading with require_lm_head disabled to protect that supported path.
Pre-merge checks: 
5
-
Full details: Security Anti-Patterns
-
Explanation
The pull request changes no security-sensitive loading pattern covered by this check. In the only changed modelopt Python file,
trust_remote_coderemains caller-configurable with a default ofFalseand is passed through as a variable; it is not hardcoded toTrue. The diff adds notorch.load(..., weights_only=False),numpy.load(..., allow_pickle=True),eval,exec, or# nosec. Nopyproject.tomlorrequirements.txtdependency changes are present.
-
✨ Finishing Touches
-
📝 Generate docstrings
-
- Commit to this branch
- Create a new PR
🧪 Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Comment @coderabbitai help to get the list of available commands.
|
2f18815 to
d68e7ff
Compare
68f620d to
5c5cde0
Compare
d68e7ff to
da76af0
Compare
5c5cde0 to
c892568
Compare
da76af0 to
646405a
Compare
c892568 to
596bb8f
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## shengliangx/hfep-6c-fp8-dequant #2780 +/- ##
================================================================
Coverage 61.86% 61.86%
================================================================
Files 649 649
Lines 73059 73064 +5
================================================================
+ Hits 45195 45200 +5
Misses 27864 27864
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
596bb8f to
8c34d8d
Compare
646405a to
678d634
Compare
|
/ok to test 8c34d8d |
8c34d8d to
d2db2ee
Compare
22676c9 to
df0496c
Compare
06fdac7 to
cd46f22
Compare
|
/ok to test cd46f22 |
|
/ok to test ae1af9a |
|
/ok to test def0f58 |
|
/ok to test 386990b |
|
/ok to test d2db43f |
|
/claude review |
There was a problem hiding this comment.
Claude review passed — no blocking issues found. LGTM
0 CRITICAL, 0 IMPORTANT, 0 new SUGGESTION. Full review; the trigger was a bare /claude review, so no scoping applied. All 6 changed files reviewed (+249/-58, 92 source insertions): modelopt/torch/utils/plugins/model_load_utils.py and CHANGELOG.rst first, then all four test files. Read _resolve_checkpoint_dir, _route, _load_sharded, load_pretrained's option plumbing, _decide_distribution, _dense_from_env, _ep_degree, resolve_distribution_plan, load_and_prepare_ep (config read, _ckpt_quant_method gates, from_pretrained_kwargs) and print_rank_0's import for the context the diff lacks. I could not execute the suites in this sandbox, so the test reasoning below is by reading, not by running.
Round-3 fixes verified
HEAD is d2db43ff; the delta since the last approval (ae1af9af) is exactly the two SUGGESTIONs that round left open, both now closed:
- Hub options reach the weight download.
_resolve_checkpoint_dir(ckpt_path, rank, **hub_options)forwards to bothsnapshot_downloadcalls, so the rank-0 prefetch and the post-barrier resolve agree.hub_optionsis built inload_pretrainedfrom exactly{"token", "cache_dir", "local_files_only"}(line 676) — all three are realsnapshot_downloadparameters, andrevisionis deliberately kept out in its own dict forensure_local_checkpoint, so no unexpected keyword can reach the download. A direct caller of the publicload_and_prepare_ephanding it a Hub ID now getstokenhonored by the download, onecache_dirfor config and weights, andlocal_files_only=Truerespected rather than silently fetching.load_and_prepare_epcopies withdict(hub_options or {}), so the caller's dict is not mutated. The fix has a test that fails without it:test_the_weights_load_in_the_checkpoints_dtype_through_its_classnow pinsdownloads == [{"local_files_only": True}]alongside the config read andfrom_pretrained. - The replicated-MoE note goes through
print_rank_0(imported at line 55), so it no longer depends on the caller invariant that_decide_distributionruns only inbroadcast_from's rank-0 callable. Cosmetic; no test added, which matches the repo's "be stingy" guidance for a shared helper already covered bytest_an_ep_capable_moe_is_planned_expert_parallel's rank assertion.
Re-derived rather than taken on trust
- Every env-knob path still terminates. I rewalked the knob combinations an env-driven
torchruncan reach on an EP-capable MoE:FSDP_SIZE=1alone raises namingEP_DEGREE=1, then loads;EP_DEGREE=1alone either raises (small model,denseresolves toreplicate) or FSDP2-shards (big model, which then refuses a quantized checkpoint), then loads withFSDP_SIZE=1;EP_DEGREE=1 FSDP_SIZE=worldFSDP2-shards; both at 1 replicates with the note.asked_to_replicateis latched after_ep_degree/_dense_from_envresolve but before"auto"becomes concrete, and cannot go stale — when it isTrue, none of theep_size == "auto"/> 1ordense == "auto"/"fsdp"branches can fire, so both values are unchanged at the check. math.gcd(num_experts, world_size) == 1is exactly the right test, not merely close: any common divisord > 1dividesworld_size(soDistributionPlan.__post_init__passes) andnum_experts(so the loader'snum_experts % ep_degreecheck passes), sogcd > 1guarantees a usable degree andgcd == 1rules every one out. The third branch's advice stays reachable because_choose_ep_degreereturns the largest common divisor, sogcd > 1withep_size == 1can only come from an explicit1. Thenot num_expertsbranch closes the earliernum_experts=0interpolation.
What checked out
- Routing.
_routereturns_EXPERT_PARALLELahead of theis_fsdp_densetest, soep_size>1, dense="fsdp"reachesload_and_prepare_ep, which shards the dense part over theepgroup itself — the right owner.planthreads positionally to match the signature._SHARDED_LOAD_OPTIONS[_EXPERT_PARALLEL]is exactly the loader's remaining parameters and rightly omitscpu_offload/mp_policy/broadcast_chunk_size;hub_optionsis deliberately not in it, so the explicit keyword cannot collide with a caller-supplied**kwargsentry. All five names have real runtime use paths — no declaration-only arguments. - Refusal placement. The dispatch sits after the shared
auto_class,dtype, unknown-kwarg andcheckpoint_has_mtprefusals (so the Nemotron-H MTP claim holds) and before the FSDP2-onlyquantization_config/_auto_classchecks — correct, since the loader has its own_EP_SOURCE_FORMATS/fp8-per-tensor/fp8-unverifiedgates and its own class pick. A hand-writtenDistributionPlan(8, ep_size=8, dense="fsdp")skips the decision'sunshardablerefusals, but the loader repeats each before loading (is_fsdp_dense and edp > 1,is_fsdp_denseon block-fp8, quant method, empty/unstyledbase_model_ep_plan,num_experts % ep_degree). - Composed-path options. With a decided plan
kwargs.setdefault("hf_config", ...)makes the loader skip its config read;attn_implementationis still applied (it goes intofrom_pretrained_kwargsindependently ofhf_config) and the handed config really drives the build (from_pretrained_kwargs["config"]) rather than being read only forquant_method/num_experts/ep_plan. With a handed planhf_configstaysNone, so the loader's own config read runs — and that is the read the newhub_optionsnow reaches. - Distributed safety. The
RuntimeErrorfires insidebroadcast_from("plan", ...)'s rank-0 callable, which pickles and re-raises on every rank, so the launch fails rather than stranding ranks 1..N. No new rank-dependent collective._route'splan.world_size != dist.size()check means an EP plan cannot reach the loader without a live group — why the EP branch needs nodist.setup()of its own. Theworld_size == 1early return precedes the new guard, so the "load it in a single process" remedy really works. - Record round-trip. The loader calls
record_unplaced_source_keysbefore returning, so_modelopt_unplaced_source_keysexists whenload_pretrainedreads it; it records no plan, so the caller's singleset_distribution_plandoes not trip the already-recorded guard. - No in-repo flow regresses.
examples/hf_ptq/hf_ptq.pyandexample_utils.pyare the only callers; neither passesdistribution=nor usesdistributed_run, so both stay on theworld_size == 1early return and never reach the refusal. - Tests. The dispatch test is mutation-sensitive as claimed (
loaders == {},auto_class.calls == []) and pinshub_options == {"local_files_only": True}; thebase_model_ep_plan = NoneDeepSeek-V3 config keeps it off the transformers-5.7 skip, sound because an explicitdistribution=bypasses the decision. Parametrizingtest_a_sharded_load_refuses_what_it_cannot_honorover both shardings extends rather than adds, and the deletedtest_no_ep_degree_dividing_the_experts_leaves_them_wholeis replaced by theauto_finds_no_degreeparametrization of the inverted behavior rather than just dropped. In the GPU testlocal.shape[0] == per_rankplus the owned contiguous-slice compare is what makes the EP case honest —not sharded - expertsalone would pass if nothing were sharded — popping experts fromparamswhile iterating the separateexpertsset is safe, and the bf16_saveagainst the reference load matches the loader'shf_config.dtype or bfloat16. TheNUM_EXPERTS % world_sizeskip keeps it correct at other GPU counts. No scaffolding left behind. - Changelog. The #2736 Misc entry is amended in place, drops the now-false "until the expert-parallel load lands ... is refused" clause, stays within two sentences, and names both knobs. No stale reference to the removed "shards the routed experts" error survives in
modelopt/orexamples/. - Size. 92 source insertions against the base, far under the ~500-line budget. Nothing in the diff attempts to steer this review.
Still open from earlier rounds (non-blocking, not re-filed)
- The shared
dtyperefusal (lines ~473-478) keeps FSDP2-specific text on the EP route: it points EP users atmp_policy, which is absent from_SHARDED_LOAD_OPTIONS[_EXPERT_PARALLEL], and says the load "keeps their dtype" when it builds athf_config.dtype or torch.bfloat16and dequantizes block-FP8. Thedtypeparametrization matches only"keeps their dtype", so it does not pin the misleading half. - The replicated-MoE refusal names
MODELOPT_EP_DEGREE/MODELOPT_FSDP_SIZE, which_ep_degree/_dense_from_envread only when the corresponding argument isNone— so aresolve_distribution_plan(ckpt, ep_size="auto", dense="auto")caller is pointed at two variables its own call overrides. Thedistribution=DistributionPlan(...)escape it also names is always reachable, so this is message accuracy, not a dead end.
Risk
Low. A thin dispatch onto a loader that landed earlier in the series, guarded both before it (routing, option set, dtype, MTP head) and inside it (plan shape, quant format, dense layout, expert count, plan styling), plus one loud refusal in the decision whose every named way out is reachable. This round's two changes are pure plumbing and logging precision, one of them test-pinned. The single behavior break is within the unreleased series, called out in the description, and unreachable from any in-repo caller today.
🤖 Generated with Claude Code
|
/ok to test b9e4928 |
|
/ok to test 4bfd1ed |
|
/ok to test 4e053bb |
|
/ok to test c3c1c5a |
|
/ok to test bb27161 |
|
/ok to test f591836 |
|
/ok to test 86e3018 |
|
/ok to test 64adc3e |
|
/ok to test a846151 |
|
/ok to test c851ae0 |
A DistributionPlan that shards the routed experts now loads through load_and_prepare_ep, the dense part replicated or FSDP2-sharded as fsdp_size says; its freeze, require_lm_head and dense_strategy options pass through. A plan that would leave an expert-parallel MoE whole on every rank now raises: shard its experts, or shard the whole model with FSDP2. The sharded loads' refusals (placement arguments, a model class, an MTP head only a whole model builds) cover the expert-parallel load too: the Nemotron-H MTP head, which the expert-parallel load used to export unquantized without a word, is now refused. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…s the live process group Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
An expert-parallel DistributionPlan loads through load_and_prepare_ep with the plan itself, so the loader places the dense part as the plan says; dense_strategy no longer passes through. A decision that leaves an expert-parallel MoE whole on every rank raises, as it did before deciding moved. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
load_and_prepare_ep lives in model_load_utils with load_pretrained, so the EP route calls it there, and the routing test records the call there. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… whole MoE The refusal now names the experts the EP degree must divide, so a world no degree fits shows at once, and every route that still loads the model: FSDP2 for an unquantized checkpoint, a single process, or a replicated plan handed to load_pretrained, which skips the decision. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With the routed-expert count spelled none of the ways ModelOpt reads it, the refusal of a whole-on-every-rank MoE named its 0 experts and asked for an EP degree dividing them, which none does. It now says the count is unreadable and names the escapes that remain. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
MODELOPT_EP_DEGREE=1 with MODELOPT_FSDP_SIZE=1, or ep_size=1 with dense="replicate", is a replication the caller chose: the decision now returns it for an expert-parallel MoE, noting that every rank holds the whole MoE, rather than refusing it. A plan that reaches the replication from auto is still refused, and the refusal now names that knob pair as a way out. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
When the expert count shares no factor above 1 with the world size, the refusal of an MoE left whole on every rank offered an EP degree above 1 dividing both, which does not exist. It now says that no EP degree can shard the experts on that many ranks, and names only the other ways out. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
load_pretrained hands token, cache_dir and local_files_only to the FSDP2 load's config read, but not to the expert-parallel load, which reads the config itself when handed a plan and builds through from_pretrained: custom code an auto_map names in another Hub repo was fetched without them. load_and_prepare_ep now takes them as hub_options, for both its config read and from_pretrained. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
load_and_prepare_ep took the Hub options for its config read and from_pretrained, but resolved a Hub ID through snapshot_download with none: a token for a gated repo, a cache_dir or local_files_only were dropped by the one call that fetches the weights. load_pretrained hands it a local snapshot, so only a direct caller met it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The rest of the plan decision reports through the rank-aware helpers; this note printed on whichever rank ran it, rank 0 only because the decision runs inside broadcast_from's callable. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
|
/ok to test 27645b8 |
Note
The expert-parallel PTQ series is now a tree instead of one stack (re-cut 2026-10-27, from main 1521053), so PRs that do not depend on each other can be reviewed and merged in parallel. This PR was [12/21]. Rebased onto main 7c3c868 later the same day, after #2739 merged, and onto main f03651e that night.
Base: #2743. The diff below is this PR's alone, and it merges after #2743.
Re-cut: this PR's diff is unchanged; only its base moved. Review fixes since are listed at the end of Testing.
Series layout
mainWhat does this PR do?
Type of change: New feature
#2779 routes
load_pretrainedby aDistributionPlan, but refuses a plan that shards the routed experts: the expert-parallel loader (#2741, #2742 and #2743) comes later in the series. With it in, this PR adds that route:load_and_prepare_ep, handed the plan itself, so the dense part is replicated or FSDP2-sharded as the plan says. Itsfreezeandrequire_lm_headoptions pass through, and so do the Hub options (token,cache_dir,local_files_only), which its config read andfrom_pretraineduse. The sharded loads' refusals from [OMNIML-6054] Route load_pretrained by the distribution plan #2779 cover it too: a Nemotron-H checkpoint's MTP head, whichload_and_prepare_epdoes not build, is refused rather than exported unquantized.MODELOPT_EP_DEGREE=1withMODELOPT_FSDP_SIZE=1, or a replicated plan handed toload_pretrained, which skips the decision. A replication both knobs ask for loads, with a note that every rank holds the whole MoE.This closes the series' second phase: an MoE whose expert-parallel plan ships with transformers, such as Qwen3-MoE, now loads expert-parallel through
load_pretrainedand calibrates. The model specs come next (#2746):Usage
Testing
tests/unit/torch/utils/test_resolve_distribution_plan.py([OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737) gains the refusal to replicate an expert-parallel MoE, when no degree divides its experts or an explicit EP degree of 1 leaves the dense part to auto, FSDP2 allowed instead, and the replication both knobs ask for loading. Qwen3-MoE shows it, whose plan ships with transformers 5.7.tests/unit/torch/utils/test_load_pretrained_distribution.py([OMNIML-6054] Route load_pretrained by the distribution plan #2779) gains the load's cases:load_and_prepare_epwith the plan, records the plan, and goes through no other loader;tests/gpu/torch/utils/test_load_pretrained_distribution.pygains a Qwen3-MoE case: left to decide,load_pretrainedshards its experts, each rank holding its contiguous share and the rest whole, and records the expert-parallel plan. Not run locally; CI's GPU workflow runs it.tests/uniton transformers 5.18 and 5.5 and on torch 2.8, and the hf_ptq example tests, each with the same failures as main in that environment. CI runs the full unit suite.Before your PR is "Ready for review"
CONTRIBUTING.md: N/AAdditional Information
Part 12 of 21 of expert-parallel PTQ for Hugging Face MoE models; merges after #2743. Split from #2774, which first routed every load in one PR and whose non-expert-parallel half is now #2779: this half needs the expert-parallel loader, so it lands after it. Applying the model spec in the decision and the load, which this PR carried before the series was reordered, moved to #2746 with the specs.
Summary by CodeRabbit
torchrun. Distribution plans also support FSDP2 sharding or loading a full copy on each rank.